//
//  ThreadViewController.m
//  objective_c_language
//
//  Created by 王玉龙 on 2018/7/11.
//  Copyright © 2018年 longlong. All rights reserved.
//

#import "ThreadViewController.h"


@interface ThreadViewController ()

{
    int _ticketCount;
}

@property (nonatomic ,strong) NSThread *thread1;
@property (nonatomic ,strong) NSThread *thread2;

@end


@implementation ThreadViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
    button.frame = CGRectMake(100, 100, 100, 100);
    button.backgroundColor = [UIColor redColor];
    [self.view addSubview:button];
    
    //设置演唱会的门票数量
    _ticketCount = 30;
    
    
    
    //先监听线程退出的通知，以便知道线程什么时候退出
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadExitNotice:) name:NSThreadWillExitNotification object:nil];
    
    //由当前线程派生出第一个其他线程时发送，一般一个线程只发送一次
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willBecomeMultiThreadedNotification:) name:NSWillBecomeMultiThreadedNotification object:nil];
    
    
    //    [self initWithTargetThread];
    //    [self detachNewThread];
    //    [self detachNewThreadWithBlock];
    
    
    [self threadAfterDelayPerform];//分线程延迟执行
    
    
    //    [self newChileThread];//回到主线程
    //常驻线程
    //    [self residentThread];
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)threadExitNotice:(NSNotification *)notification
{
    NSLog(@" threadExitNotice ------------ %@",notification.object);
}

- (void)willBecomeMultiThreadedNotification:(NSNotification *)notification
{
    NSLog(@"willBecomeMultiThreadedNotification ------------ %@",notification.object);
}

- (IBAction)buttonClick:(UIButton *)sender {
    
    //情景：某演唱会门票发售，在广州和北京均开设窗口进行销售，以下是代码实现
    
    //新建两个子线程（代表两个窗口同时销售门票）
    NSThread * window1 = [[NSThread alloc]initWithTarget:self selector:@selector(thread1) object:nil];
    window1.name = @"北京售票窗口";
    [window1 start];
    NSThread * window2 = [[NSThread alloc]initWithTarget:self selector:@selector(thread2) object:nil];
    window2.name = @"广州售票窗口";
    [window2 start];
    
    [self performSelector:@selector(saleTicket1) onThread:window1 withObject:nil waitUntilDone:NO];
    [self performSelector:@selector(saleTicket2) onThread:window2 withObject:nil waitUntilDone:NO];
}

- (void)thread1 {
    
    //我们注意到，线程启动后，执行saleTicket完毕后就马上退出了，怎样能让线程一直运行呢（窗口一直开放，可以随时指派其卖演唱会的门票的任务），答案就是给线程加上runLoop
    
    //分线程默认没有启动 runloop 的，调用-currentRunLoop，方法内部会为线程创建runloop
    NSRunLoop *runLoop1 = [NSRunLoop currentRunLoop];
    [runLoop1 runUntilDate:[NSDate date]]; //一直运行 虽然会阻塞线程，阻塞过程中并不妨碍新任务的执行。当有新任务的时候，会先执行接收到的新任务，新任务执行完之后，如果时间到了，再继续执行runUntilDate:之后的代码
    NSLog(@"runLoop1 ===== %@",runLoop1);
    
}
- (void)thread2 {
    
    NSRunLoop * runLoop2 = [NSRunLoop currentRunLoop];
    [runLoop2 runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; //自定义运行时间
    NSLog(@"runLoop2 ===== %@",runLoop2);}

//线程启动后，执行saleTicket，执行完毕后就会退出，为了模拟持续售票的过程，我们需要给它加一个循环
- (void)saleTicket1
{
    while (1)
    {
        /*
         可以看到，票的销售过程中出现了剩余数量错乱的情况，这就是前面提到的线程同步问题。
         售票是一个典型的需要线程同步的场景，由于售票渠道有很多，而票的资源是有限的，当多个渠道在短时间内卖出大量的票的时候，如果没有同步机制来管理票的数量，将会导致票的总数和售出票数对应不上的错误。
         我们在售票的过程中给票加上同步锁：同一时间内，只有一个线程能对票的数量进行操作，当操作完成之后，其他线程才能继续对票的数量进行操作
         */
        //如果还有票，继续售卖
        
        
        @synchronized (self)
        {
            
            if (_ticketCount > 0)
            {
                _ticketCount --;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数：%ld 窗口：%@", _ticketCount, [NSThread currentThread].name]);
                [NSThread sleepForTimeInterval:0.2];
            }
            else //如果已卖完，关闭售票窗口
            {
                //手动管理。比如我们让线程完成任务（售票）后自行退出，可以这样操作
                if (NSThread.currentThread.isCancelled)
                {
                    break;
                }
                else
                {
                    NSLog(@"售卖完毕");
                    /*
                     不过大家千万不要被它的名字迷惑，调用-cancel方法并不会立刻取消线程，它仅仅是将cancelled属性设置为YES。cancelled也仅仅是一个用于记录状态的属性。线程取消的功能需要我们在函数中自己实现
                     */
                    [NSThread.currentThread cancel];//给当前线程标记为取消状态
                    CFRunLoopStop(CFRunLoopGetCurrent());//停止当前线程的runLoop
                }
            }
        }
    }
    
}


- (void)saleTicket2
{
    while (1)
    {
        /*
         互斥锁：如果共享数据已经有其他线程加锁了，线程会进入休眠状态等待锁。一旦被访问的资源被解锁，则等待资源的线程会被唤醒。
         自旋锁：如果共享数据已经有其他线程加锁了，线程会以死循环的方式等待锁，一旦被访问的资源被解锁，则等待资源的线程会立即执行。
         自旋锁的效率高于互斥锁。
         
         
         
         互斥锁的优缺点：
         优点: 防止多线程对共享资源进行抢夺造成的数据安全问题
         缺点: 需要消耗大量cpu资源
         */
        //@synchronized 的作用是创建一个互斥锁
        @synchronized (self)
        {
            
            if (_ticketCount > 0)
            {
                _ticketCount --;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数：%ld 窗口：%@", _ticketCount, [NSThread currentThread].name]);
                [NSThread sleepForTimeInterval:0.2];
            }
            else //如果已卖完，关闭售票窗口
            {
                break;
            }
            
        }
    }
    
    
}

#pragma mark - init

- (void)initWithBlockThread
{
    if (@available(iOS 10.0, *))
    {
        //初始化一个NSThread对象,在 block 中执行任务。必须调用 start 方法来启动该线程
        NSThread *thread = [[NSThread alloc] initWithBlock:^{
            NSLog(@"thread ---------- %@",[NSThread currentThread]);
        }];
        thread.name = @"initWithBlock 一个线程";
        [thread start];
    }
}

- (void)initWithTargetThread
{
    //初始化一个NSThread对象,在指定的 selector 中执行任务。必须调用 start 方法来启动该线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(initWithTargetThreadSelector) object:nil];
    thread.name = @"initWithTarget 一个线程";
    [thread start];
}

/*
 使用 runloop 堵塞线程：
 //运行一次循环，在指定的模式下阻塞输入，直到给定的日期。 limitDate 截止日期
 // 如果 runloop 没有附加任何 input sources 或 timers，则此方法立即退出并返回 NO;
 // 否则，在处理完第一个 input sources 或到达 limitDate 之后返回。
 // 手动从 runloop 删除所有已知的输入源和计时器并不保证 runloop 将立即退出。
 // macOS 可以在需要的时候安装和删除额外的输入源，以处理针对接收者线程的请求。
 // 因此，这些源可以防止运行循环退出。
 - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
 
 sleepUntilDate:相当于执行一个sleep的任务。在执行过程中，即使有其他任务传入runloop，runloop也不会立即响应，必须sleep任务完成之后，才会响应其他任务
 
 //运行循环直到指定的日期，在此期间，它处理来自所有附加输入源的数据。limitDate 指定日期
 //如果 runLoop 没有任何 input sources 或 timers，则该方法立即退出;否则，它将通过反复调用 runMode:beforeDate: 直到指定的过期日期，在 NSDefaultRunLoopMode 下运行接收器。
 - (void)runUntilDate:(NSDate *)limitDate;
 runUntilDate:虽然会阻塞线程，阻塞过程中并不妨碍新任务的执行。当有新任务的时候，会先执行接收到的新任务，新任务执行完之后，如果时间到了，再继续执行runUntilDate:之后的代码
 从 runloop 中手动删除所有已知的 input sources 和 timers 并不能保证 runloop 将退出。macOS可以根据需要安装和移除附加的输入源，以处理针对接收者线程的请求。因此，这些源可以防止运行循环退出。
 */
- (void)initWithTargetThreadSelector
{
    NSLog(@"thread ---------- %@",[NSThread currentThread]);
    
    //分线程默认没有启动 runloop 的，调用-currentRunLoop，方法内部会为线程创建runloop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    NSLog(@"runLoop ===== %@",runLoop);
    
    [runLoop runUntilDate:[NSDate date]]; //一直运行 虽然会阻塞线程，阻塞过程中并不妨碍新任务的执行。当有新任务的时候，会先执行接收到的新任务，新任务执行完之后，如果时间到了，再继续执行runUntilDate:之后的代码
}

#pragma mark - detachNewThread
// runloop 实现线程保活的原理
- (void)detachNewThread
{
    //创建一个分线程，在指定的 selector 中执行任务。一旦target 完成执行 selector 方法，就会退出分线程(使用exit类方法)。如果该线程是程序中第一个分线程，该方法将带有对象nil的NSWillBecomeMultiThreadedNotification 发送到通知中心。
    //该方法没有返回 thread 对象，调用该方法后直接创建并开启一个分线程，不需要也无法调用 start 方法
    [NSThread detachNewThreadSelector:@selector(detachNewThreadSelector) toTarget:self withObject:nil];
}

- (void)detachNewThreadSelector
{
    NSDate *date = [NSDate date];
    NSLog(@"current time 1 ----- %@",date);//current time 1 ----- Mon Jul  2 14:52:08 2018
    //堵塞当前线程 10s
    [NSThread sleepUntilDate:[date dateByAddingTimeInterval:10]];
    NSLog(@"current time 2 ----- %@",[NSDate date]);//current time 2 ----- Mon Jul  2 14:52:18 2018
    
    NSLog(@"thread ---------- %@",[NSThread currentThread]);
}

- (void)detachNewThreadWithBlock
{
    if (@available(iOS 10.0, *))
    {
        [NSThread detachNewThreadWithBlock:^{
            NSThread.currentThread.name = @"阻塞线程";
            
            //            [self performSelector:@selector(detachNewThreadWithBlockClick)];
            
            
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            //            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
            
            
            //            [self performSelectorOnMainThread:@selector(mainThreadClick) withObject:nil waitUntilDone:NO];
            
            int index = 60;
            while (YES)
            {
                if (index > 0)
                {
                    index --;
                }
                else //如果已卖完，关闭售票窗口
                {
                    //手动管理。比如我们让线程完成任务（售票）后自行退出，可以这样操作
                    if (NSThread.currentThread.isCancelled)
                    {
                        //                        [NSThread exit];//执行完此句代码，立即关闭线程，后面的代码不在执行
                        //                        NSLog(@"[NSThread exit]");
                        break;
                    }
                    else
                    {
                        NSLog(@"循环完毕");
                        /*
                         不过大家千万不要被它的名字迷惑，调用-cancel方法并不会立刻取消线程，它仅仅是将cancelled属性设置为YES。cancelled也仅仅是一个用于记录状态的属性。线程取消的功能需要我们在函数中自己实现
                         */
                        [NSThread.currentThread cancel];//给当前线程标记为取消状态
                        //                        CFRunLoopStop(CFRunLoopGetCurrent());//停止当前线程的runLoop
                    }
                }
            }
            
            NSLog(@"还在执行嘛？");
            
        }];
        
        
        
        //        [NSThread detachNewThreadWithBlock:^{
        //            NSLog(@"thread ---------- %@",[NSThread currentThread]);
        //
        //            //调用堆栈地址
        //            NSLog(@"callStackReturnAddresses ---------- %@",NSThread.callStackReturnAddresses);
        //            //调用堆栈
        //            NSLog(@"callStackSymbols ---------- %@",NSThread.callStackSymbols);
        //            //thread 的堆栈大小
        //            NSLog(@"stackSize ---------- %ld",[NSThread currentThread].stackSize);
        //
        //
        //            NSLog(@"current time 1 ----- %@",[NSDate date]);//current time 1 ----- Mon Jul  2 19:20:28 2018
        //            //堵塞当前线程 10s
        //            [NSThread sleepForTimeInterval:10];
        //            NSLog(@"current time 2 ----- %@",[NSDate date]);//current time 2 ----- Mon Jul  2 19:20:38 2018
        //
        //            NSDate *date = [NSDate date];
        //            NSLog(@"current time 3 ----- %@",date);// current time 3 ----- Mon Jul  2 19:20:38 2018
        //            //堵塞当前线程 10s
        //            [NSThread sleepUntilDate:[date dateByAddingTimeInterval:10]];
        //            NSLog(@"current time 4 ----- %@",[NSDate date]);//current time 4 ----- Mon Jul  2 19:20:48 2018
        //        }];
    }
    
}

- (void)detachNewThreadWithBlockClick
{
    
    //    NSRunLoop *runLoop1 = [NSRunLoop currentRunLoop];
    //    [runLoop1 runUntilDate:[NSDate date]];
    
    NSLog(@"currentThread ==== %@",NSThread.currentThread);
}

//回到主线程更新UI
- (void)mainThreadClick
{
    NSLog(@"回到主线程更新UI ==== %@",NSThread.currentThread);
}

#pragma mark - 分线程延迟执行
//问题：分线程中调用延迟执行方法，为何不被执行？
- (void)threadAfterDelayPerform
{
    if (@available(iOS 10.0, *))
    {
        [NSThread detachNewThreadWithBlock:^{
            NSThread.currentThread.name = @"分线程延迟执行";
            //            CFRunLoopGetCurrent();
            //            CFRunLoopRun();
            //            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            //            [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:100]];
            //            [runLoop run];
            
            [self performSelector:@selector(threadAfterDelayPerformClick) withObject:self afterDelay:10];
            
            NSLog(@"分线程延迟执行");
        }];
    }
}

- (void)threadAfterDelayPerformClick
{
    NSLog(@"分线程延迟执行 ==== %@",NSThread.currentThread);
}

#pragma mark - 线程间通信
#pragma mark 事实上并没有执行，这是为什么呢？

- (void)newChileThread
{
    if (@available(iOS 10.0, *))
    {
        [NSThread detachNewThreadWithBlock:^{
            NSThread.currentThread.name = @"数据线程";
            [self performSelector:@selector(newChileThreadTask:) withObject:@{@"data":@"A"}];
        }];
    }
    //我们在新开辟的分线程执行 - performSelector: withObject: 方法，转到 newChileThreadTask: 方法执行处理数据 A 的耗时任务，在此耗时10s后，将一段数据传至主线程，由于参数 waitUntilDone 为 YES，系统在此处阻塞主线程直至 backMainThreadClick 方法里执行完毕才会接着执行分线程下面的代码，我们期望接下来执行处理数据B的任务，然后回到主线程，此时 waitUntilDone 为NO，不堵塞当前线程，分线程剩余代码 和 主线程的代码并发执行
    //通过打印结果：我们可以看到代码的执行顺序为：数据数据A-->回到主线程（阻塞当前分线程）-->执行完毕后，分线程代码接着往下执行，直到分现成处理完毕数据，线程关闭。在主线程的代码：[self performSelector:@selector(newChileThreadTask:) onThread:thread withObject:@{@"data":@"B"} waitUntilDone:NO];事实上并没有执行，这是为什么呢？
}

- (void)newChileThreadTask:(id)sender
{
    NSLog(@"sender ====== %@",sender);
    if ([sender[@"data"] isEqualToString:@"A"])
    {
        [NSThread sleepForTimeInterval:10];//处理耗时任务
        NSLog(@"---------- 处理数据A -----------  %@",[NSDate date]);
        //任务A处理完毕，回调主线程执行某些操作，并阻塞当前线程直至主线程的操作完成
        [self performSelectorOnMainThread:@selector(backMainThreadClick:) withObject:@{@"result":@"1",@"thread":NSThread.currentThread} waitUntilDone:YES];
    }
    
    NSLog(@"已从主线程返回 --------------------  %@",sender);
    
    if ([sender[@"data"] isEqualToString:@"B"])
    {
        [NSThread sleepForTimeInterval:10];//处理耗时任务
        NSLog(@"---------- 处理数据B -----------  %@",[NSDate date]);
        [self performSelectorOnMainThread:@selector(backMainThreadClick:) withObject:@{@"result":@"2",@"thread":NSThread.currentThread} waitUntilDone:NO];
    }
    
    
    NSLog(@"---------- 处理后续数据了 ----------- %@",[NSDate date]);
    [NSThread sleepUntilDate:[[NSDate date] dateByAddingTimeInterval:10]];//接着处理耗时任务
    
    NSLog(@"---------- 处理数据完毕 ----------- %@",[NSDate date]);
}


- (void)backMainThreadClick:(id)sender
{
    NSLog(@"sender ------ %@",sender);
    NSThread *thread = sender[@"thread"];
    [NSThread sleepForTimeInterval:3];//阻塞主线程 3s
    
    if ([sender[@"result"] isEqualToString:@"1"])
    {
        [self performSelector:@selector(newChileThreadTask:) onThread:thread withObject:@{@"data":@"B"} waitUntilDone:NO];
    }
    else if ([sender[@"result"] isEqualToString:@"2"])
    {
        [self performSelector:@selector(newChileThreadTask:) onThread:thread withObject:@{@"data":@"C"} waitUntilDone:NO];
    }
    
}

#pragma mark - 常驻线程
// 问题：NSMachPort 是什么？为什么能保活分线程？
- (void)residentThread
{
    if (@available(iOS 10.0, *))
    {
        //常驻线程
        [NSThread detachNewThreadWithBlock:^{
            NSThread.currentThread.name = @"常驻线程";
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            
            //通过给runLoop添加监听的端口，保持线程不被销毁
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
            NSLog(@"---------- 此处代码不被执行 -----------");
        }];
    }
}

@end

/* https://blog.ibireme.com/2015/05/18/runloop/
 RunLoop的寄生于线程：一个线程只能有唯一对应的RunLoop，但这个根RunLoop里可以嵌套子RunLoop，主线程的RunLoop自动创建，子线程的RunLoop默认不创建，在子线程中调用NSRunLoop.current获取RunLoop对象的时候，就会创建RunLoop
 一般来讲，一个线程一次只能执行一个任务，执行完成后线程就会退出。如果我们需要一个机制，让线程能随时处理事件但并不退出，通常的代码逻辑是这样的：
 RunLoop 与线程的关系
 1、主线程在程序运行的时候会创建唯一的一个MainRunLoop(根runLoop,实际测试还可以嵌套,因为它时while循环为前提的,在一个runLoop上可以嵌套其他的runLoop),在这里称最上层的RunLoop为根runLoop.
 2、线程与RunLoop之间是一一对应的,子线程需要通过CFGetCurrentRunLoop才能获取对应的runLoop。
 3、线程销毁的时候，和它绑定的runLoop也会一起销毁。
 
 
 如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒?
 Runloop 是和线程相关的基础部分。一个Runloop是一个不停的接受事件,处理事件,等待唤醒和睡眠的死循环。一个Runloop的目的是有任务的时候保持线程忙碌，没有任务的时候线程休眠。
 在没有事情做的时候,它会进入睡眠状态,释放CPU资源,在程序接受到事件后,它会从消息队列(messageQueue)中一条一条的将事件取出,然后进行执行；每次runLoop过程中它还进行了 界面刷新,GCD分派事件的调度,系统内核MarchPort事件处理，以及系统强制插入的程序中的其它紧急事件处理。
 
 
 当在线程上执行一个selector的时候，该线程必须有一个活跃的Runloop，对于你创建的线程，这意味着一直等待到你的代码显示的开启run loop。
 run loop每进行一次循环就会处理队列化的 performSelector 的调用，而不是每次run loop循环处理队列中的选一个处理。
 
 
 
 
 当前线程直接调用
 - (id)performSelector:(SEL)aSelector;
 - (id)performSelector:(SEL)aSelector withObject:(id)object;
 
 //NSTimer 或者 performSelector:afterDelay: 是通过 CFRunLoop 定时器实现的
 //延迟执行：使当前线程的定时器去调用 afterDelay 方法,，实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。但是我们知道,在子线程中,默认是没有定时器的,所以delayTest方法将没有被调用的机会.
 - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay
 
 
 @interface NSObject (NSDelayedPerforming)
 
 //在当前线程上下一个run loop回路中执行selector，并附加了延迟选项。因为它等待下一个run loop回路到来才执行selector，这些方法从当前执行的代码中提供了一个自动的微小延迟。多个排队的selector会按照顺序一个一个的执行。
 - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
 - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
 + (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
 + (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
 
 @end
 
 
 
 @interface NSRunLoop (NSOrderedPerform)
 
 - (void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
 - (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
 - (void)cancelPerformSelectorsWithTarget:(id)target;
 
 @end
 
 
 
 @interface NSObject (NSThreadPerformAdditions)
 
 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
 // equivalent to the first method with kCFRunLoopCommonModes
 
 //当调用 performSelector:onThread: 时，实际上其会创建一个 Timer 加到对应的线程去，同样的，如果对应线程没有 RunLoop 该方法也会失效。
 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
 // equivalent to the first method with kCFRunLoopCommonModes
 - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
 
 @end
 
 
 */

